package com.zbf.wangguan.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zbf.common.entity.AuthErrorMessage;
import com.zbf.common.utils.JWTUtis;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * author: LCG
 * 日期: 2020/5/31 14:41
 * 描述: 全局过滤器，用来过滤用户是否登录
 */
@Component
public class WangGuanGloablFilter implements GlobalFilter,Ordered {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;


    /**
     * 无需登录认证的路径  这是Map结构的数据的获取方式
     */
    @Value("#{${spring.noNeedLoginPath}}")
    private Map<String,String> noNeedLoginPath;


    /**
     * 判断登录以及Token是否过期、重新生成Token的逻辑
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //判断当前请求的路径是不是需要登录，如果不需要则会直接通过返回一个Mono 否则的话返回一个null
        Mono<Void> needToLogin = this.isNeedToLogin(exchange, chain);
        if(needToLogin!=null){
            return needToLogin;
        }
        //获取请求头中的Token串
        List<String> tokenList = exchange.getRequest().getHeaders().get("token");
        //检测是否有Token信息
        Mono<Void> hasToken = this.hasToken(tokenList,exchange);
        if(hasToken!=null){
           return  hasToken;
        }
        //校验Token是不是过期，如果没有过期的话进行延期否则返回重新登录
        return hasUsefulToken(tokenList,exchange,chain);

    }


    /**
     * 过滤器的执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }


    /**
     * 未授权登录也就是没有Token或者是token过期的情况下
     * @param exchange
     * @param errorData  给定的错误信息
     * @return
     */
    public Mono<Void> responseUNAUTHORIZED(ServerWebExchange exchange,String errorData){
        ServerHttpResponse response = exchange.getResponse();
        HttpStatus httpStatus = HttpStatus.OK;
        byte[] bits = errorData.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(httpStatus == null ? HttpStatus.OK : httpStatus);
        //指定编码，否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }


    /**
     * 校验请求中是否携带Token参数,如果没有的话返回失败信息
     * @param exchange
     * @return
     */
    private Mono<Void> hasToken(List<String> tokenList,ServerWebExchange exchange){
        if(tokenList.size()==0||tokenList.get(0).equals("")||tokenList.get(0).equals("null")){
            //校验有没有Token参数
            Map<String,Object> map=new HashMap<>(8);
            map.put("code",AuthErrorMessage.NO_LOGIN.getCode());
            map.put("error",AuthErrorMessage.NO_LOGIN.getMessage());
            return this.responseUNAUTHORIZED(exchange, JSON.toJSONString(map));
        }else{
            return null;
        }

    }

    /**
     * 检测是否有有用的Token的，如果可用则对Token进行延续，否则的话返回错误信息重新登录
     * @return
     */
    private Mono<Void> hasUsefulToken(List<String> tokenList,ServerWebExchange exchange,GatewayFilterChain chain){

        try{
            //如果过期的话这个解析的过程会报错 ExpiredJwtException
            JSONObject jsonObject = JWTUtis.decodeJwtTocken(tokenList.get(0));
            //获取登录时存储的用户token信息
            String userKey="USERINFO" + jsonObject.getLong("id");
            //获取Redis中存储的Token信息
            String toekn = redisTemplate.opsForValue().get(userKey);
            //校验登录时生成的token和传入的token是否一致(防止两个请求一前一后的请求过来，
            // 然后先请求的修改了Redis中的Token后请求的使用的依然是旧的token)
            if (toekn != null || toekn.equals(tokenList.get(0))) {
                //重新生成token
                //如果解析过程没有报错的话进行Token的延期(其实就是重新生成一下Token信息)
                String newToken = JWTUtis.generateToken(jsonObject.toJSONString());
                //将新生成的token重新存入redis
                redisTemplate.opsForValue().set(userKey, newToken);
                //将新的Token写入响应头带回客户端
                exchange.getResponse().getHeaders().set("newToken",newToken);
                //正常的返回客户端
                return chain.filter(exchange);
            } else {
                //让前端跳转到登录界面
                //校验有没有Token参数或者是Token参数过期
                Map<String,Object> map=new HashMap<>(8);
                map.put("code",AuthErrorMessage.NO_LOGIN.getCode());
                map.put("error",AuthErrorMessage.NO_LOGIN.getMessage());
                return responseUNAUTHORIZED(exchange,JSON.toJSONString(map));
            }
        }catch (ExpiredJwtException e){
            //校验有没有Token参数或者是Token参数过期
            Map<String,Object> map=new HashMap<>(8);
            map.put("code",AuthErrorMessage.HAS_EXPIRE.getCode());
            map.put("error",AuthErrorMessage.HAS_EXPIRE.getMessage());
            return this.responseUNAUTHORIZED(exchange, JSON.toJSONString(map));
        }
    }

    /**
     * 判断当前请求的路径是不是需要登录操作
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> isNeedToLogin(ServerWebExchange exchange, GatewayFilterChain chain){
        //获取请求的路径
        ServerHttpRequest request = exchange.getRequest();
        //获取当前的请求路径
        String routPath = request.getPath().pathWithinApplication().value().split("\\?")[0];

        //判断当前访问的路径是不是需要登录认证
        if(noNeedLoginPath.containsKey(routPath)){
            //如果访问的路径不需要登录认证则直接通过
            return chain.filter(exchange);
        }else{
            return null;
        }
    }

}
